EcoCommons Marxan MaPP connection

EcoCommons -> Marxan MaPP connection

Author: Zhao Xiang, EcoCommons

Date: 2024-09-24

Introduction

Using the Species distribution modeling techniques provided by the EcoCommons Platform (www.ecocommons.org.au), we produced probability distribution maps for the three Queensland endangered species: koala, brush tailed rock-wallaby, and beach stone curlew.

Then we adjusted the probability distribution maps of these three species with the planning units shapefile prepared by the Marxan MaPP, and ran four planning scenarios with a target of expanding the coverage of protected areas in QLD to 30%.

EcoCommons Outputs

  1. Species records pulled from GBIF, ALA, EcoPlots, OBIS
  2. Species distribution modelling output: Species distribution Probability maps (This is the input tested in this project).

Marxan MaPP Inputs

  1. Shapefile of planning area and units.
  2. Shapefile of cost.
  3. Shapefile and csv of biodiversity features (Where EcoCommons can help!).

EcoCommons connects with Marxan Showcase:

Make sure you are in the directory you want

getwd()
[1] "/Users/zhaoxiang/Documents/tmp/ecocommons-marxan/ecocommons-marxan-integration-poc"

Create a virtual environment to install and load all essential packages

# install.packages("renv") # install this package if you have not.
renv::init()

Install and load essential packages

install.packages("sf")
The following package(s) will be installed:
- sf [1.0-17]
These packages will be installed into "~/Documents/tmp/ecocommons-marxan/ecocommons-marxan-integration-poc/renv/library/macos/R-4.4/x86_64-apple-darwin20".

# Installing packages --------------------------------------------------------
- Installing sf ...                             OK [linked from cache]
Successfully installed 1 package in 10 milliseconds.
# First specify the packages of interest
packages = c("sf", "terra", "ggplot2", "ggspatial", "raster", "dplyr", "shiny", "httpuv", "rmarkdown", "knitr", "jsonlite", "reticulate", "htmltools", "pryr")

# Now load or install&load all. This process will take a long time since we are using a virtual environment and install a lot of packages.
package.check <- lapply(
  packages,
  FUN = function(x) {
    if (!require(x, character.only = TRUE)) {
      install.packages(x, dependencies = TRUE)
      library(x, character.only = TRUE)
     }
  }
 )

# You might need to link your renv to proj
Sys.setenv(PROJ_LIB = "/usr/local/Cellar/proj/9.4.1/share/proj") # Update path if different

renv::snapshot()
- The lockfile is already up to date.

1. We get the QLD planning units from Marxan MaPP

# Helper function to convert bytes to GB
mem_in_gb <- function(memory_in_bytes) {
  return(memory_in_bytes / (1024^3))  # Convert bytes to GB
}

# Record memory usage before reading the shapefile
cat("Memory usage before reading shapefile: ", mem_in_gb(pryr::mem_used()), " GB\n")


QLD_Unit <- "qld_3species_Marxan/QLD_plannningunits/cost-surface-template.shp"  #This cost-surface-template was prepared by the Marxan Mapp with a resolution of 189 Km2, which is the highest resolution Marxan Mapp can give at this scale.

QLD_Unit  <- st_read(QLD_Unit)
st_crs(QLD_Unit) <- 4326  # Example with WGS84 CRS
QLD_Unit  <- st_simplify(QLD_Unit , dTolerance = 0.01) 

# Record memory usage after reading the shapefile
cat("Memory usage after reading shapefile: ", mem_in_gb(pryr::mem_used()), " GB\n")

# Calculate the resolution since Marxan MaPP for visulization purpose
areas <- st_area(QLD_Unit)
areas_numeric <- as.numeric(areas)
average_area <- mean(areas_numeric)

# Record memory usage after calculating the resolution
cat("Memory usage after calculating areas: ", mem_in_gb(pryr::mem_used()), " GB\n")

# Convert to numeric
average_area_km2 <- average_area / 1e6

# Get the number of rows
n_rows <- nrow(QLD_Unit)

# Plot the shapefile with no fill color and number of rows in the title
ggplot(data = QLD_Unit) +
  geom_sf(fill = NA, color = "gray") +
  theme_minimal() +
  ggtitle(paste("QLD Planning Units:", n_rows, "\n",
                "Resolution of planning in square kilometers:", round(average_area_km2)))+
  theme(plot.title = element_text(hjust = 0.5))  # Center the title

2. I made a cost layer using the reciprocal of the distance to state-owned road as a surrogate of the cost.

The assumption is: the closer to the state owned road, the more expensive to purchase the unit.

QLD_cost_road <- st_read("qld_3species_Marxan/QLD_Cost/QLD_cost_road.shp")

# Plot the shapefile with continuous cost_road values
ggplot(QLD_cost_road) +
  geom_sf(aes(fill = cost_road)) +
  scale_fill_continuous(name = "Cost",
                        low = "lightblue", high = "red",
                        labels = c("0 (Low cost)", "1 (High cost)"),
                        breaks = c(0.01, 1)) +
  theme_minimal() +
  labs(title = "Cost: using the distance to road of each Unit as a proxy")+
  theme(plot.title = element_text(hjust = 0.5))  # Center the title

3. Biodiversity features. I used EcoCommons to produce three species’ SDM to start with.

  • Species 1: koala

  • Species 2: brush tailed rock-wallaby

  • Species 3: beach stone curlew

# Define the folder path where the rasters are stored
folder_path <- "qld_3species_Marxan/QLD_feature/"

# Get a list of all .tif files in the folder
raster_files <- list.files(path = folder_path, pattern = "\\.tif$", full.names = TRUE)

# Extract the species names from the file names (removing the folder path and .tif extension)
species_names <- tools::file_path_sans_ext(basename(raster_files))

# Read all raster files in one go using lapply
raster_list <- lapply(raster_files, rast)  # Use rast() from terra for reading rasters

# Using QLD_Unit as the spatial vector for masking

# Transform the raster CRS to match the vector CRS and apply masking in one step
raster_list <- lapply(raster_list, function(r) {
  r_transformed <- project(r, crs(vect(QLD_Unit)))
  mask(r_transformed, vect(QLD_Unit))
})

# Function to convert rasters to data frames and combine them
prepare_raster_data <- function(raster_list, species_names) {

  # Initialize an empty data frame
  combined_df <- data.frame()
  # Loop through each raster and combine them into one data frame
  for (i in seq_along(raster_list)) {
    # Convert raster to a data frame
    raster_df <- as.data.frame(raster_list[[i]], xy = TRUE)
    # Rename the third column to 'value' or any appropriate name for the raster values
    names(raster_df)[3] <- "value"
    # Add a column to identify the species name
    raster_df$species <- species_names[i]
    # Combine the raster data with the overall data frame
    combined_df <- bind_rows(combined_df, raster_df)
}
  return(combined_df)
}

# Prepare the combined data frame
combined_raster_df <- prepare_raster_data(raster_list, species_names)

# Create the ggplot with facet_wrap to display each raster in a separate facet
ggplot(combined_raster_df, aes(x = x, y = y, fill = value)) +  # Use the correct column name for fill
  geom_raster()+
  facet_wrap(~ species, ncol = 3) +  # Adjust ncol to control the number of columns
  scale_fill_viridis_c() +  # You can adjust the color scale as needed
  labs(title = "Species SDM") +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5))

4. We need to turn these SDMs to binary results (shapefies).

5. Plot species SDM binary shapefile outputs for double check

output_dir <- "qld_3species_Marxan/QLD_feature/Marxan_feature_input/"

# List all the shapefiles in the directory (assuming each species has its own shapefile)
species_files <- list.files(output_dir, pattern = "\\.shp$", full.names = TRUE)

species_files

# Extract species names from the filenames (you can adjust this depending on your naming conventions)
species_names <- tools::file_path_sans_ext(basename(species_files))

# Load all species shapefiles and add a species identifier
species_sf_list <- lapply(seq_along(species_files), function(i) {
  sf <- st_read(species_files[i])
  sf$species <- species_names[i]  # Add species name column
  return(sf)
})

# Combine all species into one dataset
combined_species_sf <- do.call(rbind, species_sf_list)

# Plot the unit (base map) first and overlay the species habitats without borders
combined_plot_with_unit <- ggplot() +
  geom_sf(data = QLD_Unit, fill = NA, color = "grey", linewidth = 0.01) +  # Base map (QLD Unit)
  geom_sf(data = combined_species_sf, aes(fill = species), color = NA) +  # No borders for species
  scale_fill_manual(values = RColorBrewer::brewer.pal(n = length(species_names), name = "Set1")) +  # Automatically assign colors
  theme_minimal() +
  labs(title = "Species Habitats within QLD Unit",
       subtitle = paste(species_names, collapse = ", ")) +  # List all species in subtitle
  theme(legend.title = element_blank())

# Display the plot
print(combined_plot_with_unit)

6. We can also make a species presence and absence csv table.

# Function to extract presence (1) and absence (0) from raster based on a threshold (e.g., 0.5)

extract_presence_absence <- function(raster_data, unit) {
  extracted_values <- extract(raster_data, vect(unit), fun = mean, na.rm = TRUE)
  presence_absence <- ifelse(extracted_values[, 2] >= 0.5, 1, 0)
  return(presence_absence)
}

# Create an empty presence-absence data frame
presence_absence_df <- data.frame(puid = QLD_Unit$puid)  # Assuming 'puid' is the unique identifier

# Loop through each species raster in the raster list and extract presence-absence data
for (i in seq_along(raster_list)) {
  # Generate a dynamic presence column name for the current species
  presence_col_name <- paste0(species_names[i], "_presence")
  
  # Extract presence/absence data and add it to the presence-absence dataframe
  presence_absence_df[[species_names[i]]] <- extract_presence_absence(raster_list[[i]], QLD_Unit)
}

# Write the final presence-absence data frame to a CSV file
output_csv <- file.path(output_dir, "presence_absence_species.csv")
write.csv(presence_absence_df, output_csv, row.names = FALSE)

# Check the CSV output
print(head(presence_absence_df))
  puid beach_stone_curlew_GLM brushtailed_rockwallaby_GLM Koala_GLM
1    1                      0                           0         0
2    2                      0                           0         0
3    3                      0                           0         0
4    4                      0                           0         0
5    5                      0                           0         0
6    6                      0                           0         0

Marxan Four scenarios solutions:

Our SDMs input to Marxan MaPP:

EcoCommons SDMs output of three species on Marxan MaPP

Scenario 1: No SDMs, No Costs

No Costs, neither SDMs

Scenario 2: SDMS, No Costs

SDMs only

Scenario 3: Costs, No SDMs

Costs only

Scenario 4: SDMs + Costs

Costs and SDMs